/*###########################################################
# © 2013-2016 Daniel 'grindhold' Brendle
#
# This file is part of Nostril.
#
# Nostril is free software: you can redistribute it and/or 
# modify it under the terms of the GNU Affero General Public License 
# as published by the Free Software Foundation, either 
# version 3 of the License, or (at your option) any later 
# version.
#
# Nostril is distributed in the hope that it will be 
# useful, but WITHOUT ANY WARRANTY; without even the implied 
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
# PURPOSE. See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public 
# License along with Nostril. 
# If not, see http://www.gnu.org/licenses/.
###########################################################*/

using Gtk;
using Gee;
using Gdk;

namespace nostril {
    namespace gui {
        public abstract class Viewer {
            public static ViewerType get_viewer_type() {return ViewerType.NONE;}
            public abstract void render(uint8[] data);
            public abstract Gtk.Widget get_responsible_widget();
        }

        public enum ViewerType {
            REQUEST,
            RESPONSE,
            BOTH,
            NONE
        }

        public class Gui : DataRenderer {
            private Gtk.Builder builder = null;
            private Gtk.Window window = null;
            private Gtk.Statusbar status = null;
            private Gtk.TreeView list_view = null;
            private Gtk.ListStore list_store = null;
            private Gtk.TreeModelFilter list_store_filter = null;
            private Gtk.TextBuffer request_buffer = null;
            private Gtk.TextBuffer response_buffer = null;
            private Gtk.Notebook requesttabs = null;
            private Gtk.Notebook responsetabs = null;
            private ArrayList<Viewer> requestviewers = new ArrayList<Viewer>();
            private ArrayList<Viewer> responseviewers = new ArrayList<Viewer>();

            private uint status_context = 0;

            private string filter_string = "";
            private static const int NO_ID_TO_CHECK = -1;
            private int id_to_check = Gui.NO_ID_TO_CHECK;
            private uint pending_messages = 0;
            private static const int NO_MSG_DISPLAYED = -1;
            private int id_currently_displayed = Gui.NO_MSG_DISPLAYED;
            private bool rendering = false;

            public Gui(nostril.data.MessageStore store){
                base(store);
                this.builder = new Gtk.Builder();
                try {
                    this.builder.add_from_file(Main.DATAPATH+"ui/nostril.ui");
                } catch (Error e){
                    stderr.printf("Could not load UI file %s - File not found\n",Main.DATAPATH+"ui/nostril.ui");
                }

                this.builder.connect_signals(this);
                
                this.request_buffer = this.builder.get_object("request_buffer") as Gtk.TextBuffer;
                this.response_buffer = this.builder.get_object("response_buffer") as Gtk.TextBuffer;

                this.list_view = this.builder.get_object("listview") as Gtk.TreeView;
                this.list_store = this.builder.get_object("list_store") as Gtk.ListStore;
                this.list_store_filter = this.builder.get_object("list_store_filter") as Gtk.TreeModelFilter;


                this.status = this.builder.get_object("statusbar") as Gtk.Statusbar;
                this.status_context = this.status.get_context_id("maincontext");
                this.status.push(this.status_context, "welcome to nostril");

                this.requesttabs = this.builder.get_object("requesttabs") as Gtk.Notebook;
                this.responsetabs = this.builder.get_object("responsetabs") as Gtk.Notebook;

                // Initialize attribute assignments for the columns
                var pending_renderer = this.builder.get_object("pending") as Gtk.CellRenderer;
                var mime_icon_renderer = this.builder.get_object("mime_icon") as Gtk.CellRenderer;
                var pending_column = this.builder.get_object("pending_col") as Gtk.TreeViewColumn;
                pending_column.add_attribute(pending_renderer, "active", 0);
                pending_column.add_attribute(pending_renderer, "pulse", 6);
                pending_column.add_attribute(pending_renderer, "visible", 0);
                pending_column.add_attribute(mime_icon_renderer, "pixbuf", 7);
                var id_renderer = this.builder.get_object("id") as Gtk.CellRenderer;
                var id_column = this.builder.get_object("id_col") as Gtk.TreeViewColumn;
                id_column.add_attribute(id_renderer, "text", 1);
                id_column.sort_indicator = true;
                id_column.sort_order = SortType.DESCENDING;
                id_column.sort_column_id = 1;
                var status_renderer = this.builder.get_object("status") as Gtk.CellRenderer;
                var status_column = this.builder.get_object("status_col") as Gtk.TreeViewColumn;
                status_column.add_attribute(status_renderer, "text", 2);
                var host_renderer = this.builder.get_object("host") as Gtk.CellRenderer;
                var host_column = this.builder.get_object("host_col") as Gtk.TreeViewColumn;
                host_column.add_attribute(host_renderer, "text", 3);
                var path_renderer = this.builder.get_object("path") as Gtk.CellRenderer;
                var path_column = this.builder.get_object("path_col") as Gtk.TreeViewColumn;
                path_column.add_attribute(path_renderer, "text", 4);
                var size_renderer = this.builder.get_object("size") as Gtk.CellRenderer;
                var size_column = this.builder.get_object("size_col") as Gtk.TreeViewColumn;
                size_column.add_attribute(size_renderer, "text", 5);

                this.list_view.set_search_column(3);
                this.list_store_filter.set_visible_func(this.filter_function);

                Timeout.add (50, () => {
                    this.list_store.foreach ((model, path, iter) => {
                        Value val;
                        this.list_store.get_value (iter, 6, out val);
                        val.set_double (val.get_double () + 1);
                        this.list_store.set_value (iter, 6, val);
                        return false;
                    });
                    return true;
                });

                this.initialize_viewers();
                this.window = this.builder.get_object("mainwindow") as Gtk.Window;
                this.window.show_all();
            }

            private void initialize_viewers(){
                //TODO: Find out how it would be possible to work with metaprogramming. So that there
                //      can be a list of Viewers somewhere and the viewers do not have to be added 
                //      manually
                Viewer v;
                Gtk.Widget viewerwidget;
                Gtk.Label label;
                if (RawViewer.get_viewer_type() == ViewerType.REQUEST || RawViewer.get_viewer_type() == ViewerType.BOTH){
                    v = new RawViewer();
                    viewerwidget = v.get_responsible_widget();
                    label = new Gtk.Label("Raw");
                    this.requesttabs.append_page(viewerwidget, label);
                    this.requestviewers.add(v);
                }
                if (RawViewer.get_viewer_type() == ViewerType.RESPONSE || RawViewer.get_viewer_type() == ViewerType.BOTH){
                    v = new RawViewer();
                    viewerwidget = v.get_responsible_widget();
                    label = new Gtk.Label("Raw");
                    this.responsetabs.append_page(viewerwidget, label);
                    this.responseviewers.add(v);
                }
                if (HexViewer.get_viewer_type() == ViewerType.REQUEST || HexViewer.get_viewer_type() == ViewerType.BOTH){
                    v = new HexViewer();
                    viewerwidget = v.get_responsible_widget();
                    label = new Gtk.Label("Hex");
                    this.requesttabs.append_page(viewerwidget, label);
                    this.requestviewers.add(v);
                }
                if (HexViewer.get_viewer_type() == ViewerType.RESPONSE || HexViewer.get_viewer_type() == ViewerType.BOTH){
                    v = new HexViewer();
                    viewerwidget = v.get_responsible_widget();
                    label = new Gtk.Label("Hex");
                    this.responsetabs.append_page(viewerwidget, label);
                    this.responseviewers.add(v);
                }
            }

            private string size_string(int64 size){
                int64 tmp_size = size;
                const string[] units = {"B", "kiB", "MiB", "GiB", "TiB", "EiB"};
                int i = 0;
                while (tmp_size > 1024 && i < 5 ){
                    tmp_size /= 1024;
                    i++;
                }
                return "%lld %s".printf(tmp_size, units[i]);
            }

            public bool filter_function(Gtk.TreeModel model, Gtk.TreeIter iter){
                if (this.filter_string != ""){
                    GLib.Value val;
                    model.get_value(iter, 3, out val);
                    return val.get_string().contains(this.filter_string);
                }
                return true;
            }

            public bool foreach_tree_new_entry(Gtk.TreeModel treemodel, Gtk.TreePath path, Gtk.TreeIter iter){
                Gtk.ListStore model = treemodel as Gtk.ListStore;
                nostril.data.MessageStore store = this.data_source as nostril.data.MessageStore;
                GLib.Value id_val;
                Gdk.Pixbuf le_pixbuf;
                this.list_store.get_value(iter, 1, out id_val);
                int id = id_val.get_int();
                if (id == this.id_to_check){
                    try {
                        nostril.data.Message msg = store.get_message(id);
                        unowned uint status = msg.status_code;
                        unowned int64 size = msg.size;

                        string content_type = "";
                        MapIterator<string, string> mapiter = msg.response_headers.map_iterator();
                        while (mapiter.next()){
                            if (mapiter.get_key().down() == "content-type"){
                                content_type = mapiter.get_value();
                            }
                        }
                        
                        
                        if (content_type != ""){
                            GLib.Icon icon = GLib.ContentType.get_icon(content_type);
                            le_pixbuf = null;
                            try {
                                le_pixbuf = Gtk.IconTheme.get_default().lookup_by_gicon(icon, 16, Gtk.IconLookupFlags.NO_SVG).load_icon();
                            } catch (Error e) {
                                model.set(iter, 0,false, 2,"%u".printf(status), 5, this.size_string(size));
                            }
                            model.set(iter, 0,false, 2,"%u".printf(status), 5,this.size_string(size), 7,le_pixbuf);
                        }else{
                            model.set(iter, 0,false, 2,"%u".printf(status), 5,this.size_string(size));
                        }
                        this.pending_messages--;
                    } catch (nostril.data.MessageStoreException.NO_MESSAGE_WITH_ID e){
                        model.remove(iter);
                    } finally {
                        this.id_to_check = Gui.NO_ID_TO_CHECK;
                    }
                    return true;
                }
                return false;
            }

            public override void render(){ 
                if (this.rendering)
                    return;
                this.rendering = true;
                nostril.data.MessageStore store = this.data_source as nostril.data.MessageStore;
                int messages = store.get_message_count();

                Gtk.TreeIter adding_iter;
                Iterator<int> interesting_messages = store.get_interesting_messages();
                while (interesting_messages.next()){
                    int id = interesting_messages.get();
                    this.id_to_check = id;
                    this.list_store.foreach(this.foreach_tree_new_entry);
                    if (!(this.id_to_check == Gui.NO_ID_TO_CHECK)){
                        try{
                            nostril.data.Message msg = store.get_message(id);
                            this.pending_messages++;
                            this.list_store.insert(out adding_iter,0);
                            this.list_store.set(adding_iter, 0,true, 1,id, 2,"…", 3,msg.hostname, 4,msg.path, 5,"…");
                        } catch (nostril.data.MessageStoreException.NO_MESSAGE_WITH_ID e){

                        }
                        this.id_to_check = Gui.NO_ID_TO_CHECK;
                    }
                }
                
                store.reset_interesting_messages();

                this.request_buffer.set_text("");
                this.response_buffer.set_text("");
                if (this.id_currently_displayed != Gui.NO_MSG_DISPLAYED){
                    try {
                        nostril.data.Message current_msg = store.get_message(this.id_currently_displayed);
                        MapIterator<string,string> map_iter;
                        this.request_buffer.set_text("Loading Request");
                        this.response_buffer.set_text("Loading Response");

                        string request_string = "";
                        map_iter = current_msg.request_headers.map_iterator();
                        while (map_iter.next()){
                            request_string += "%s: %s\r\n".printf(map_iter.get_key(), map_iter.get_value());
                        }
                        this.request_buffer.set_text(request_string);

                        foreach(Viewer v in this.requestviewers) {
                            v.render(current_msg.request_body.data);
                        }
             
                        string response_string = "";
                        map_iter = current_msg.response_headers.map_iterator();
                        while (map_iter.next()){
                            response_string += "%s: %s\r\n".printf(map_iter.get_key(), map_iter.get_value());
                        }
                        this.response_buffer.set_text(response_string);

                        foreach(Viewer v in this.responseviewers) {
                            v.render(current_msg.response_body.data);
                        }

                    } catch (nostril.data.MessageStoreException.NO_MESSAGE_WITH_ID e){
                    }
                }

                this.status.pop(this.status_context);
                this.status.push(this.status_context, "%d calls transceived, %u pending".printf(messages,this.pending_messages));
                this.rendering = false;
            }

            [CCode (instance_pos = -1)]
            public void close_program(Gtk.ImageMenuItem source){
                Gtk.main_quit();
            }

            [CCode (instance_pos = -1)]
            public void clear_all(Gtk.ToolButton source){
                nostril.data.MessageStore store = this.data_source as nostril.data.MessageStore;
                this.id_currently_displayed = Gui.NO_MSG_DISPLAYED;
                store.clear();
            }

            [CCode (instance_pos = -1)]
            public void clear_selected(Gtk.ImageMenuItem source){
                Gtk.TreeModel model = this.list_view.get_model();
                Gtk.TreeSelection selection = this.list_view.get_selection();
                GLib.List<Gtk.TreePath> sel = selection.get_selected_rows(out model);
                if (sel.length() != 0){
                    ArrayList<int> to_delete = new ArrayList<int>();
                    sel.foreach((path) =>{
                        Gtk.TreeIter selected;
                        GLib.Value val;
                        model.get_iter(out selected, path);
                        model.get_value(selected, 1, out val);
                        to_delete.add(val.get_int());
                    });
                    nostril.data.MessageStore store = this.data_source as nostril.data.MessageStore;
                    store.clear_messages(to_delete);
                }
            }

            [CCode (instance_pos = -1)]
            public void toggled_recording(Gtk.ToggleToolButton source){
                nostril.data.MessageStore store = this.data_source as nostril.data.MessageStore;
                store.set_recording(source.get_active());
            }

            [CCode (instance_pos = -1)]
            public void filter_changed(Gtk.Entry source){
                this.filter_string = source.get_text();
            }


            [CCode (instance_pos = -1)]
            public void selected_request(Gtk.TreeView source){
                Gtk.TreeModel model = source.get_model();
                Gtk.TreeSelection selection = source.get_selection();
                GLib.List<Gtk.TreePath> sel = selection.get_selected_rows(out model);
                if (sel.length() != 0){
                    Gtk.TreeIter selected;
                    model.get_iter(out selected, sel.nth_data(0));

                    GLib.Value val;
                    model.get_value(selected, 1, out val);
                    this.id_currently_displayed = val.get_int();
                    this.render();
                }
            }

            [CCode (instance_pos = -1)]
            public void show_about(Gtk.ImageMenuItem source){
                if (About.is_active) {
                    return;
                }
                new About();
            }
        }

        public class About : Object {
            private Gtk.Builder builder = null;
            private Gtk.AboutDialog about = null;

            private static bool _is_active = false;
            public static bool is_active {
                get {
                    return About._is_active;
                }
            }

            public About(){
                this.builder = new Gtk.Builder();
                try {
                    this.builder.add_from_file(Main.DATAPATH+"ui/nostril.ui");
                } catch (Error e){
                    stderr.printf("Could not load UI file ui/nostril.ui\n");
                }
                this.about = this.builder.get_object("about") as Gtk.AboutDialog;
                About._is_active = true;
                this.about.run();
                About._is_active = false;
                this.about.destroy();
            }
        }
    }
}
